import { buildin, format, jsonOption, } from "./golang";

function jsToGo(scope, typename, options) {
  let typeMap = {};
  let typeDeclare = '';
  let parentKey = '';

  let result = jsToGo0(scope, typename, options);
  result.go += typeDeclare;
  return result;

  // return prefix+key if exist prefix
  function getTypeName() {
    // return key+number. number plus 1 if key has been record
    function getTypeNameWithCount() {
      let count = typeMap[parentKey];
      if (count !== undefined) {
        return format(parentKey + (typeMap[parentKey]++));
      }
      typeMap[parentKey] = 0;
      return format(parentKey);
    }

    let typeNameWithCount = getTypeNameWithCount();
    let prefix = options.prefix;
    if (prefix === undefined) {
      return typeNameWithCount;
    }
    return format(prefix + typeNameWithCount);
  }

  function jsToGo0(scope, typename, options) {
    let go = '';
    let tabs = 0;
    let nested = options.nested;

    typename = format(typename || 'AutoGenerated');
    append(`type ${typename} `);

    parseScope(scope);

    return {go: go};

    function parseScope(scope, depth = 0) {
      let type = goType(scope);
      switch (type) {
        case buildin.slice:
          parseSlice(scope, depth);
          break;
        case buildin.struct:
          parseStruct(scope, depth + 1);
          break;
        default:
          append(type)
      }
    }

    function parseSlice(array, depth = 0) {
      let sliceLength = array.length;

      append('[]');

      switch (sliceLength) {
        case 0:
          parseScope(null, depth);
          return;
        case 1:
          parseScope(array[0], depth);
          return;
        default:
      }

      let sliceType = goSliceType(array);

      switch (sliceType) {
        case buildin.struct:
          let structFields = {};
          for (let ele of array) {
            let keys = Object.keys(ele);
            for (let keyName of keys) {
              if (!structFields.hasOwnProperty(keyName)) {
                structFields[keyName] = {
                  count: 0,
                  fieldTypeMap: {}
                }
              }
              let value = ele[keyName];
              structFields[keyName].fieldTypeMap[goType(value)] = value;
              structFields[keyName].count++;
            }
          }

          let structFieldNames = Object.keys(structFields), struct = {}, omitempty = {}, nullable = {};
          for (let fieldName of structFieldNames) {
            let elem = structFields[fieldName];
            let fieldTypeMapNames = Object.keys(elem.fieldTypeMap);
            switch (fieldTypeMapNames.length) {
              case 1:
                struct[fieldName] = elem.fieldTypeMap[fieldTypeMapNames[0]];
                break;
              case 2:
                let filterNames = fieldTypeMapNames.filter(n => n !== buildin.interface);
                if (filterNames.length === 1) {
                  struct[fieldName] = elem.fieldTypeMap[filterNames[0]];
                  nullable[fieldName] = true;
                  break;
                }
              // eslint-disable-next-line no-fallthrough
              default:
                struct[fieldName] = null;
                break;
            }
            omitempty[fieldName] = elem.count !== sliceLength;
          }

          parseStruct(struct, depth + 1, omitempty, nullable);
          break;
        case buildin.slice:
          let flatten = [];
          for (let slice of array) {
            flatten = flatten.concat(slice);
          }
          parseSlice(flatten, depth);
          return;
        default:
          append(sliceType || buildin.interface);
      }
    }

    function parseStruct(object, depth = 0, omitempty = undefined, nullable = undefined) {
      if (depth > 1 && !nested) {
        let typeName = getTypeName();
        append(typeName);
        let nest = jsToGo0(object, typeName, options);
        typeDeclare += `\n\n${nest.go}`;
        return
      }

      append('struct {\n');
      ++tabs;
      let keys = Object.keys(object);
      for (let i in keys) {
        if (keys.hasOwnProperty(i)) {
          let keyName = keys[i];
          indent(tabs);
          append(format(keyName) + ' ');
          if (nullable && nullable[keyName]) {
            append('*');
          }
          parentKey = keyName;
          parseScope(object[keyName], depth);
          parentKey = '';

          parseTags(keyName, options, omitempty);
          append('\n');
        }
      }
      indent(--tabs);
      append('}');
    }

    function parseTags(keyName, options, omitempty) {
      if (!options || !options.tags || options.tags.length === 0) {
        return
      }
      append(' `');
      let tagResult = '';
      let tags = options.tags;
      for (let tag of tags) {
        tagResult += `${tag.key}:"${tag.isSnake ? camelToSnake(keyName) : keyName}`;
        if (omitempty && omitempty[keyName] === true) {
          tagResult += (',' + jsonOption.omitempty);
        }
        tagResult += '" '
      }
      append(tagResult.trim());
      append('`');
    }

    function indent(tabs) {
      for (let i = 0; i < tabs; i++)
        go += '\t';
    }

    function append(str) {
      go += str;
    }
  }
}

function goType(val) {
  if (val === null) {
    return buildin.interface;
  } else if (Array.isArray(val)) {
    return buildin.slice;
  }

  switch (typeof val) {
    case 'string':
      if (/\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(\+\d\d:\d\d|Z)/.test(val))
        return buildin.time;
      else
        return buildin.string;
    case 'number':
      if (val % 1 === 0) {
        if (val > -2147483648 && val < 2147483647)
          return buildin.int;
        else
          return buildin.int64;
      } else
        return buildin.float64;
    case 'boolean':
      return buildin.bool;
    case 'object':
      return buildin.struct;
    default:
      return buildin.interface;
  }
}

function goSliceType(array) {
  let sliceType = null;
  for (let ele of array) {
    let thisType = goType(ele);
    if (sliceType === null)
      sliceType = thisType;
    else if (sliceType !== thisType) {
      sliceType = mostSpecificPossibleGoType(thisType, sliceType);
      if (sliceType === buildin.interface)
        break;
    }
  }
  return sliceType
}

function camelToSnake(str) {
  if (typeof str !== 'string') {
    return '';
  }
  if (str.length < 2) {
    return str.toLowerCase();
  }
  str += 'L';
  let index = 0, strings = [];
  for (let i in str) {
    if (i === '0') {
      continue;
    }
    let decimal = str[i].charCodeAt(0);
    if (decimal >= 65 && decimal <= 90) {
      strings.push(str.substring(index, i).toLowerCase());
      index = i;
    }
  }
  return strings.join('_');
}

// Given two types, returns the more specific of the two
function mostSpecificPossibleGoType(typ1, typ2) {
  if (typ1.substr(0, 5) === 'float'
    && typ2.substr(0, 3) === 'int')
    return typ1;
  else if (typ1.substr(0, 3) === 'int'
    && typ2.substr(0, 5) === 'float')
    return typ2;
  else
    return buildin.interface;
}

export default jsToGo;
